%%%-------------------------------------------------------------------
%%% File        : msg.erl
%%% Author      : Fyodor Ustinov <ufm@ufm.su>
%%% Description : MailEr Project
%%%               msg helper to STORAGE service,
%%%
%%% Created :  3 Sep 2009 by Fyodor Ustinov <ufm@ufm.su>
%%%-------------------------------------------------------------------
-module(msg).

-behaviour(gen_server).

%% API
-export([start_link/1]).

%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
	 terminate/2, code_change/3]).

-include("storage.hrl").


%%====================================================================
%% API
%%====================================================================
%%--------------------------------------------------------------------
%% Function: start_link() -> {ok,Pid} | ignore | {error,Error}
%% Description: Starts the server
%%--------------------------------------------------------------------
start_link(P) ->
    gen_server:start_link(?MODULE, [P], [{spawn_opts,[{fullsweep_after,0}]}]).

%%====================================================================
%% gen_server callbacks
%%====================================================================

%%--------------------------------------------------------------------
%% Function: init(Args) -> {ok, State} |
%%                         {ok, State, Timeout} |
%%                         ignore               |
%%                         {stop, Reason}
%% Description: Initiates the server
%%--------------------------------------------------------------------
init([{Id, Node, Queue}]) ->
    {ok, #mail{id = Id, node = Node, queue = Queue}}.

%%--------------------------------------------------------------------
%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} |
%%                                      {reply, Reply, State, Timeout} |
%%                                      {noreply, State} |
%%                                      {noreply, State, Timeout} |
%%                                      {stop, Reason, Reply, State} |
%%                                      {stop, Reason, State}
%% Description: Handling call messages
%%--------------------------------------------------------------------

handle_call({add_kludge, A}, _From, M) ->
    Rc = try
	     {ok, K} = mlib:add_kludge(M#mail.kludges, A),
	     {reply, ok, M#mail{kludges = K}}
	 catch
	     _:_ ->
		 {reply, false, M}
	 end,
    Rc
;

%%
%% add_line
%%

handle_call({add_line, A}, _From, M)
  when is_binary(A), M#mail.lines =:= ?MAX_LINES_PER_CHUNK ->
    Rc = try
	     ok = flush(M),
	     {reply, ok, M#mail{buffsize = M#mail.buffsize + erlang:byte_size(A),
				buffer = [A], 
				lines = 1, 
				chunks = M#mail.chunks + 1}
	     }
	 catch
	     _:_ ->
		 {reply, error, M}
	 end,
    Rc
;

handle_call({add_line, A}, _From, M)
  when is_binary(A) ->
    {reply, ok, M#mail{buffer = [A | M#mail.buffer],
		       buffsize = M#mail.buffsize + erlang:byte_size(A),
		       lines = M#mail.lines + 1}
    }
;

%%
%% store
%%
handle_call(store, _From, M) ->
    Rc = try 
	     if
		 M#mail.chunks =:= 0 andalso M#mail.lines < ?MAX_LINES_NO_BLOB ->
		     Mn = clear_dirty(M),
		     ok = mnesia:activity(transaction,
					  fun() -> mnesia:write(?MAIL, Mn, write) end, 
					  mnesia_frag),
		     {reply, ok, Mn};
		 
		 M#mail.lines =/= 0 ->
		     ok = flush(M),
		     Mn = M#mail{lines = 0, buffer = {M#mail.id, 0}},
		     ok = save(set_dirty(Mn)),
		     {reply, ok, clear_dirty(Mn)};

%%		 M#mail.chunks =/= 0 andalso M#mail.lines =:= 0 ->
%%		     Mn = M#mail{lines = 0, buffer = {M#mail.id, 0}},
%%		     ok = save(set_dirty(Mn)),
%%		     {reply, ok, clear_dirty(Mn)};

		 is_tuple(M#mail.buffer) ->
io:format("is_tuple: '~p'~n",[M]),
		     ok = save(M),
		     {reply, ok, clear_dirty(M)};

		 true ->
		     {reply, error, M}
	     end
	 catch
	     _:_ ->
		 {reply, error, M}
	 end,
    Rc
;


%%
%% get_ functions
%%
handle_call(get_size, _From, M) ->     {reply, {ok, M#mail.buffsize},  M};
handle_call(get_sanity, _From, M) ->   {reply, {ok, M#mail.sanity},    M};
handle_call(get_rcptto, _From, M) ->   {reply, {ok, M#mail.rcpt_to},   M};
handle_call(get_mailfrom, _From, M) -> {reply, {ok, M#mail.mail_from}, M};
handle_call(get_fromip, _From, M) ->   {reply, {ok, M#mail.from_ip},   M};


%% -- INTERNAL COMMANDS --

handle_call(get_chunks, _From, M) ->
    {reply, {ok, M#mail.chunks}, M}
;

handle_call(get_struct, _From, M) ->
    {reply, {ok, M}, M}
;

handle_call(get_lines, _From, M) ->
    {reply, {ok, M#mail.lines}, M}
;

    

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

handle_call(_Request, _From, State) ->
    Reply = error,
    {reply, Reply, State}
.

%%--------------------------------------------------------------------
%% Function: handle_cast(Msg, State) -> {noreply, State} |
%%                                      {noreply, State, Timeout} |
%%                                      {stop, Reason, State}
%% Description: Handling cast messages
%%--------------------------------------------------------------------
%% commands do not required answer

handle_cast(done, M) ->
    {stop, normal, M}
;

handle_cast(die, M) ->
    delete(M),
    {stop, normal, M}
;    

handle_cast({duplicate, F}, M) ->
    {ok, Mn} = F(get_struct,[]),
    Mnn = set_dirty(Mn),
    Sid = M#mail.id,
    {noreply, Mnn#mail{id = Sid}}
;

handle_cast({set_time, Time}, M) ->
    {noreply, M#mail{time = Time}}
;

handle_cast({set_myhostname, H}, M) ->
    {noreply, M#mail{myhostname = H}}
;

handle_cast({set_sanity, S}, M) ->
    {noreply, M#mail{sanity = S}}
;

handle_cast({change_sanity, S}, M) ->
    {noreply, M#mail{sanity = M#mail.sanity + S}}
;

handle_cast({set_fromip, A}, M) ->
    {noreply, M#mail{from_ip = A}}
;

handle_cast({set_fromname, A}, M) ->
    {noreply, M#mail{from_name = A}}
;

handle_cast({set_mailfrom, A}, M) ->
    {noreply, M#mail{mail_from = A}}
;

handle_cast({set_rcptto, A}, M) ->
    {noreply, M#mail{rcpt_to = A}}
;

handle_cast({add_rcptto, A}, M) ->
    {noreply, M#mail{rcpt_to = [A | M#mail.rcpt_to]}}
;

handle_cast({set_hello, A}, M) ->
    {noreply, M#mail{hello = A}}
;

handle_cast({set_kludges, A}, M) ->
    {noreply, M#mail{kludges = A}}
;

handle_cast({add_newkludge, {Kl, Val}}, M) ->
    {noreply, M#mail{kludges = mlib:add_newkludge(M#mail.kludges, Kl, Val)}}
;

handle_cast({set, P, A}, M) ->
    {noreply, M#mail{attr = lists:keystore(P, 1, M#mail.attr, {P, A})}}
;
handle_cast({add, P, A}, M) ->
    Nt = case lists:keysearch(P, 1, M#mail.attr) of
	     {value, T} ->
		 {P, element(2,T) ++ [A]};
	     false ->
		 {P, A}
	 end,
    {noreply,M#mail{attr = lists:keyreplace(P, 1, M#mail.attr, Nt)}}
; 



handle_cast(_Msg, State) ->
    io:format("MSG cast: '~p'~n",[_Msg]),
    {noreply, State}
.

%%--------------------------------------------------------------------
%% Function: handle_info(Info, State) -> {noreply, State} |
%%                                       {noreply, State, Timeout} |
%%                                       {stop, Reason, State}
%% Description: Handling all non call/cast messages
%%--------------------------------------------------------------------
handle_info(_Info, State) ->
io:format("MSG info: '~p'~n",[_Info]),
    {noreply, State}.

%%--------------------------------------------------------------------
%% Function: terminate(Reason, State) -> void()
%% Description: This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any necessary
%% cleaning up. When it returns, the gen_server terminates with Reason.
%% The return value is ignored.
%%--------------------------------------------------------------------
terminate(Reason, M) ->
io:format("MSG terminate: '~p'~n",[Reason]),
    case Reason of
	normal -> % Normal shutdown. msg is closed
	    ok;
	shutdown -> % dirty close msg. clean blob if need
	    delete(M),
	    ok;
	_Else ->
	    delete(M),
	    ok
    end
.

%%--------------------------------------------------------------------
%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState}
%% Description: Convert process state when code is changed
%%--------------------------------------------------------------------
code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

%%--------------------------------------------------------------------
%%% Internal functions
%%--------------------------------------------------------------------


%%
%% work with dirty flag.
%%
is_dirty(M)
  when is_record(M, mail) ->
    case lists:keysearch(dirty, 1, M#mail.attr) of
	{value, {dirty, true}} ->
	    true;
	_Else ->
	    false
    end
.

set_dirty(M)
  when is_record(M, mail) ->
    M#mail{attr = lists:keystore(dirty, 1, M#mail.attr, {dirty, true})}
.

clear_dirty(M)
  when is_record(M, mail) ->
    M#mail{attr = lists:keydelete(dirty, 1, M#mail.attr)}
.

%%
%% save message
%%

save(M)
  when is_record(M, mail) ->
    F = fun() ->
		case is_dirty(M) of
		    true ->
			[Stor] = mnesia:read(?BLOB, M#mail.buffer, write),
			{value, {refs, Nch}} = lists:keysearch(refs, 1, Stor#blob.buffer),
			case lists:member(M#mail.id, Nch) of
			    false ->
				Kn = Stor#blob{buffer = lists:keyreplace(
							  refs, 
							  1, 
							  Stor#blob.buffer,
							  {refs, [M#mail.id | Nch]}
							 )
					      },
				ok = mnesia:write(?BLOB, Kn, write);
			    true ->
				ok
			end;
		    false ->
			ok
		end,
		ok = mnesia:write(?MAIL, clear_dirty(M), write)
	end,
    ok = mnesia:activity(transaction, F, mnesia_frag),
    ok
.

%%
%% Delete message
%%

delete(M) 
  when is_record(M, mail) ->
    F = fun() ->
		if
		    is_tuple(M#mail.buffer) ->
			[Stor] = mnesia:read(?BLOB, M#mail.buffer, write),
			{value, {refs, Nch}} = lists:keysearch(refs, 1, Stor#blob.buffer),
			case Nch -- [M#mail.id] of
			    [] -> % last link
				{value, {chunks, Chn}} = lists:keysearch(chunks, 1, Stor#blob.buffer),
				ok = delete_chunks(Stor#blob.id, Chn);

			    T -> % not last link
				Kn = Stor#blob{buffer = lists:keyreplace(
							  refs, 
							  1, 
							  Stor#blob.buffer,
							  {refs, T}
							 )
					      },
				ok = mnesia:write(?BLOB, Kn, write)
			end;

                    true -> % No blobbed mail
			ok
		end,
		ok = mnesia:delete(?MAIL, M#mail.id, write)
	end,
    ok = mnesia:activity(transaction, F, mnesia_frag),
    ok
.


delete_chunks(ID, 0) ->
    mnesia:delete(?BLOB, {ID, 0}, write),
    ok
;

delete_chunks(ID, Chn) ->
     mnesia:delete(?BLOB, {ID, Chn}, write),
     delete_chunks(ID,Chn - 1)
.


flush(M) ->
    Rc = try
	     Bn = lists:reverse(M#mail.buffer),
	     Stor = #blob{id = {M#mail.id, M#mail.chunks+1}, buffer = Bn},
	     F = fun() ->
			 case mnesia:read(?BLOB, {M#mail.id, 0}, write) of
			     [] -> % Not exist
				 ok = mnesia:write(?BLOB,
						   #blob{id = {M#mail.id, 0}, 
							 buffer=[{refs, [M#mail.id]}, {chunks, 1}]
							},
						   write
						  ),
				 ok;

			     [K] ->
				 {value, Nch} = lists:keysearch(chunks, 1, K#blob.buffer),
				 Kn = K#blob{buffer = lists:keyreplace(
							chunks, 
							1, 
							K#blob.buffer,
							{chunks, element(2, Nch) + 1}
						       )
					    },
				 ok = mnesia:write(?BLOB,Kn, write);

			     _Else ->
				 mnesia:abort(bad_record)

			 end,
			 ok = mnesia:write(?BLOB, Stor, write)
		 end,
	     ok = mnesia:activity(transaction, F, mnesia_frag),
	     ok
	 catch
	     _:_ ->
		 error
	 end,
    Rc
.

